// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/platform-defs.h>
#include <ddk/protocol/composite.h>
#include <usb/usb-request.h>
#include <fbl/auto_lock.h>
#include <hw/reg.h>
#include <pretty/hexdump.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "dwc3.h"
#include "dwc3-regs.h"
#include "dwc3-types.h"

// MMIO indices
enum {
  MMIO_USB3OTG,
};

// IRQ indices
enum {
  IRQ_USB3,
};

// Component indices
enum {
  COMPONENT_PDEV,
  COMPONENT_UMS,
  COMPONENT_COUNT,
};

void dwc3_print_status(dwc3_t* dwc) {
  auto* mmio = dwc3_mmio(dwc);
  auto dsts = DSTS::Get().ReadFrom(mmio);
  zxlogf(TRACE, "DSTS: ");
  zxlogf(TRACE, "USBLNKST: %d ", dsts.USBLNKST());
  zxlogf(TRACE, "SOFFN: %d ", dsts.SOFFN());
  zxlogf(TRACE, "CONNECTSPD: %d ", dsts.CONNECTSPD());
  if (dsts.DCNRD())
    zxlogf(TRACE, "DCNRD ");
  if (dsts.SRE())
    zxlogf(TRACE, "SRE ");
  if (dsts.RSS())
    zxlogf(TRACE, "RSS ");
  if (dsts.SSS())
    zxlogf(TRACE, "SSS ");
  if (dsts.COREIDLE())
    zxlogf(TRACE, "COREIDLE ");
  if (dsts.DEVCTRLHLT())
    zxlogf(TRACE, "DEVCTRLHLT ");
  if (dsts.RXFIFOEMPTY())
    zxlogf(TRACE, "RXFIFOEMPTY ");
  zxlogf(TRACE, "\n");

  auto gsts = GSTS::Get().ReadFrom(mmio);
  zxlogf(TRACE, "GSTS: ");
  zxlogf(TRACE, "CBELT: %d ", gsts.CBELT());
  zxlogf(TRACE, "CURMOD: %d ", gsts.CURMOD());
  if (gsts.SSIC_IP())
    zxlogf(TRACE, "SSIC_IP ");
  if (gsts.OTG_IP())
    zxlogf(TRACE, "OTG_IP ");
  if (gsts.BC_IP())
    zxlogf(TRACE, "BC_IP ");
  if (gsts.ADP_IP())
    zxlogf(TRACE, "ADP_IP ");
  if (gsts.Host_IP())
    zxlogf(TRACE, "HOST_IP ");
  if (gsts.Device_IP())
    zxlogf(TRACE, "DEVICE_IP ");
  if (gsts.CSRTimeout())
    zxlogf(TRACE, "CSR_TIMEOUT ");
  if (gsts.BUSERRADDRVLD())
    zxlogf(TRACE, "BUSERRADDRVLD ");
  zxlogf(TRACE, "\n");
}

static void dwc3_stop(dwc3_t* dwc) {
  auto* mmio = dwc3_mmio(dwc);

  fbl::AutoLock lock(&dwc->lock);

  DCTL::Get().ReadFrom(mmio).set_RUN_STOP(0).set_CSFTRST(1).WriteTo(mmio);
  while (DCTL::Get().ReadFrom(mmio).CSFTRST()) {
    usleep(1000);
  }
}

static void dwc3_start_peripheral_mode(dwc3_t* dwc) {
  auto* mmio = dwc3_mmio(dwc);

  dwc->lock.Acquire();

  // configure and enable PHYs
  GUSB2PHYCFG::Get(0).ReadFrom(mmio).set_USBTRDTIM(9).WriteTo(mmio);
  GUSB3PIPECTL::Get(0)
      .ReadFrom(mmio)
      .set_DELAYP1TRANS(0)
      .set_SUSPENDENABLE(0)
      .set_LFPSFILTER(1)
      .set_SS_TX_DE_EMPHASIS(1)
      .WriteTo(mmio);

  // configure for device mode
  GCTL::Get()
      .FromValue(0)
      .set_PWRDNSCALE(2)
      .set_U2RSTECN(1)
      .set_PRTCAPDIR(GCTL::PRTCAPDIR_DEVICE)
      .set_U2EXIT_LFPS(1)
      .WriteTo(mmio);

  uint32_t nump = 16;
  uint32_t max_speed = DCFG::DEVSPD_SUPER;
  DCFG::Get().ReadFrom(mmio).set_NUMP(nump).set_DEVSPD(max_speed).set_DEVADDR(0).WriteTo(mmio);

  dwc3_events_start(dwc);

  dwc->lock.Release();

  dwc3_ep0_start(dwc);

  dwc->lock.Acquire();

  // start the controller
  DCTL::Get().FromValue(0).set_RUN_STOP(1).WriteTo(mmio);

  dwc->lock.Release();
}

static zx_status_t xhci_get_protocol(void* ctx, uint32_t proto_id, void* protocol) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  // XHCI uses same MMIO and IRQ as dwc3, so we can just share our pdev protoocl
  // with the XHCI driver
  return device_get_protocol(dwc->pdev_dev, proto_id, protocol);
}

static void xhci_release(void* ctx) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  fbl::AutoLock lock(&dwc->usb_mode_lock);

  if (dwc->start_device_on_xhci_release) {
    dwc3_start_peripheral_mode(dwc);
    dwc->start_device_on_xhci_release = false;
    dwc->usb_mode = USB_MODE_PERIPHERAL;
  }
}

static zx_protocol_device_t xhci_device_ops = []() {
  zx_protocol_device_t device;
  device.version = DEVICE_OPS_VERSION;
  device.get_protocol = xhci_get_protocol;
  device.release = xhci_release;
  return device;
}();

static void dwc3_start_host_mode(dwc3_t* dwc) {
  auto* mmio = dwc3_mmio(dwc);

  dwc->lock.Acquire();

  // configure for host mode
  GCTL::Get()
      .FromValue(0)
      .set_PWRDNSCALE(2)
      .set_U2RSTECN(1)
      .set_PRTCAPDIR(GCTL::PRTCAPDIR_HOST)
      .set_U2EXIT_LFPS(1)
      .WriteTo(mmio);

  dwc->lock.Release();

  // add a device to bind the XHCI driver
  ZX_DEBUG_ASSERT(dwc->xhci_dev == nullptr);

  zx_device_prop_t props[] = {
      {BIND_PLATFORM_DEV_VID, 0, PDEV_VID_GENERIC},
      {BIND_PLATFORM_DEV_PID, 0, PDEV_PID_GENERIC},
      {BIND_PLATFORM_DEV_DID, 0, PDEV_DID_USB_XHCI},
  };

  device_add_args_t args = {};
  args.version = DEVICE_ADD_ARGS_VERSION;
  args.name = "dwc3";
  args.proto_id = ZX_PROTOCOL_PDEV;
  args.ctx = dwc;
  args.ops = &xhci_device_ops;
  args.props = props;
  args.prop_count = countof(props);

  zx_status_t status = device_add(dwc->parent, &args, &dwc->xhci_dev);
  if (status != ZX_OK) {
    zxlogf(ERROR, "dwc3_start_host_mode failed to add device for XHCI: %d\n", status);
  }
}

void dwc3_usb_reset(dwc3_t* dwc) {
  zxlogf(INFO, "dwc3_usb_reset\n");

  dwc3_ep0_reset(dwc);

  for (unsigned i = 2; i < countof(dwc->eps); i++) {
    dwc3_ep_end_transfers(dwc, i, ZX_ERR_IO_NOT_PRESENT);
    dwc3_ep_set_stall(dwc, i, false);
  }

  dwc3_set_address(dwc, 0);
  dwc3_ep0_start(dwc);
  usb_dci_interface_set_connected(&dwc->dci_intf, true);
}

void dwc3_disconnected(dwc3_t* dwc) {
  zxlogf(INFO, "dwc3_disconnected\n");

  dwc3_cmd_ep_end_transfer(dwc, EP0_OUT);
  dwc->ep0_state = EP0_STATE_NONE;

  if (dwc->dci_intf.ops) {
    usb_dci_interface_set_connected(&dwc->dci_intf, false);
  }

  for (unsigned i = 2; i < countof(dwc->eps); i++) {
    dwc3_ep_end_transfers(dwc, i, ZX_ERR_IO_NOT_PRESENT);
    dwc3_ep_set_stall(dwc, i, false);
  }
}

void dwc3_connection_done(dwc3_t* dwc) {
  auto* mmio = dwc3_mmio(dwc);

  dwc->lock.Acquire();

  uint32_t speed = DSTS::Get().ReadFrom(mmio).CONNECTSPD();
  uint16_t ep0_max_packet = 0;

  switch (speed) {
    case DSTS::CONNECTSPD_HIGH:
      dwc->speed = USB_SPEED_HIGH;
      ep0_max_packet = 64;
      break;
    case DSTS::CONNECTSPD_FULL:
      dwc->speed = USB_SPEED_FULL;
      ep0_max_packet = 64;
      break;
    case DSTS::CONNECTSPD_SUPER:
    case DSTS::CONNECTSPD_ENHANCED_SUPER:
      dwc->speed = USB_SPEED_SUPER;
      ep0_max_packet = 512;
      break;
    default:
      zxlogf(ERROR, "dwc3_connection_done: unsupported speed %u\n", speed);
      dwc->speed = USB_SPEED_UNDEFINED;
      break;
  }

  dwc->lock.Release();

  if (ep0_max_packet) {
    dwc->eps[EP0_OUT].max_packet_size = ep0_max_packet;
    dwc->eps[EP0_IN].max_packet_size = ep0_max_packet;
    dwc3_cmd_ep_set_config(dwc, EP0_OUT, USB_ENDPOINT_CONTROL, ep0_max_packet, 0, true);
    dwc3_cmd_ep_set_config(dwc, EP0_IN, USB_ENDPOINT_CONTROL, ep0_max_packet, 0, true);
  }

  usb_dci_interface_set_speed(&dwc->dci_intf, dwc->speed);
}

void dwc3_set_address(dwc3_t* dwc, unsigned address) {
  auto* mmio = dwc3_mmio(dwc);
  fbl::AutoLock lock(&dwc->lock);

  DCFG::Get().ReadFrom(mmio).set_DEVADDR(address).WriteTo(mmio);
}

void dwc3_reset_configuration(dwc3_t* dwc) {
  auto* mmio = dwc3_mmio(dwc);

  dwc->lock.Acquire();

  // disable all endpoints except EP0_OUT and EP0_IN
  DALEPENA::Get().FromValue(0).EnableEp(EP0_OUT).EnableEp(EP0_IN).WriteTo(mmio);

  dwc->lock.Release();

  for (unsigned i = 2; i < countof(dwc->eps); i++) {
    dwc3_ep_end_transfers(dwc, i, ZX_ERR_IO_NOT_PRESENT);
    dwc3_ep_set_stall(dwc, i, false);
  }
}

static zx_status_t dwc3_cancel_all(void* ctx, uint8_t ep) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  unsigned ep_num = dwc3_ep_num(ep);
  if (ep_num >= 32) {
    return ZX_ERR_INVALID_ARGS;
  }
  fbl::AutoLock l(&dwc->eps[ep_num].lock);
  if (dwc->eps[ep_num].current_req) {
    dwc3_cmd_ep_end_transfer(dwc, ep);
  }
  if (list_is_empty(&dwc->eps[ep_num].queued_reqs)) {
    return ZX_OK;
  }
  list_node_t list;
  list_move(&dwc->eps[ep_num].queued_reqs, &list);
  dwc_usb_req_internal_t* entry;
  l.release();
  list_for_every_entry (&list, entry, dwc_usb_req_internal_t, node) {
    usb_request_complete(INTERNAL_TO_USB_REQ(entry), ZX_ERR_IO_NOT_PRESENT, 0, &entry->complete_cb);
  };
  return ZX_OK;
}

static void dwc3_request_queue(void* ctx, usb_request_t* req, const usb_request_complete_t* cb) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  auto* req_int = USB_REQ_TO_INTERNAL(req);
  req_int->complete_cb = *cb;

  zxlogf(LTRACE, "dwc3_request_queue ep: %u\n", req->header.ep_address);
  unsigned ep_num = dwc3_ep_num(req->header.ep_address);
  if (ep_num < 2 || ep_num >= countof(dwc->eps)) {
    zxlogf(ERROR, "dwc3_request_queue: bad ep address 0x%02X\n", req->header.ep_address);
    usb_request_complete(req, ZX_ERR_INVALID_ARGS, 0, cb);
    return;
  }

  dwc3_ep_queue(dwc, ep_num, req);
}

static zx_status_t dwc3_set_interface(void* ctx, const usb_dci_interface_protocol_t* dci_intf) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  memcpy(&dwc->dci_intf, dci_intf, sizeof(dwc->dci_intf));
  return ZX_OK;
}

static zx_status_t dwc3_config_ep(void* ctx, const usb_endpoint_descriptor_t* ep_desc,
                                  const usb_ss_ep_comp_descriptor_t* ss_comp_desc) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  return dwc3_ep_config(dwc, ep_desc, ss_comp_desc);
}

static zx_status_t dwc3_disable_ep(void* ctx, uint8_t ep_addr) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  return dwc3_ep_disable(dwc, ep_addr);
}

static zx_status_t dwc3_set_stall(void* ctx, uint8_t ep_address) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  return dwc3_ep_set_stall(dwc, dwc3_ep_num(ep_address), true);
}

static zx_status_t dwc3_clear_stall(void* ctx, uint8_t ep_address) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  return dwc3_ep_set_stall(dwc, dwc3_ep_num(ep_address), false);
}

static size_t dwc3_get_request_size(void* ctx) {
  // Allocate dwc_usb_req_internal_t after usb_request_t, to accommodate queueing in
  // the dwc3 layer.
  return sizeof(usb_request_t) + sizeof(dwc_usb_req_internal_t);
}

usb_dci_protocol_ops_t dwc_dci_ops = {.request_queue = dwc3_request_queue,
                                      .set_interface = dwc3_set_interface,
                                      .config_ep = dwc3_config_ep,
                                      .disable_ep = dwc3_disable_ep,
                                      .ep_set_stall = dwc3_set_stall,
                                      .ep_clear_stall = dwc3_clear_stall,
                                      .get_request_size = dwc3_get_request_size,
                                      .cancel_all = dwc3_cancel_all};

static zx_status_t dwc3_set_mode(void* ctx, usb_mode_t mode) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  zx_status_t status = ZX_OK;

  if (mode == USB_MODE_OTG) {
    return ZX_ERR_NOT_SUPPORTED;
  }

  fbl::AutoLock lock(&dwc->usb_mode_lock);

  if (dwc->usb_mode == mode) {
    return ZX_OK;
  }

  // Shutdown if we are in peripheral mode
  if (dwc->usb_mode == USB_MODE_PERIPHERAL) {
    dwc3_events_stop(dwc);
    dwc->irq_handle.reset();
    dwc3_disconnected(dwc);
    dwc3_stop(dwc);
  } else if (dwc->usb_mode == USB_MODE_HOST) {
    if (dwc->xhci_dev) {
      device_remove(dwc->xhci_dev);
      dwc->xhci_dev = nullptr;

      if (mode == USB_MODE_PERIPHERAL) {
        dwc->start_device_on_xhci_release = true;
        return ZX_OK;
      }
    }
  }

  dwc->start_device_on_xhci_release = false;
  if (dwc->ums.ops != nullptr) {
    status = usb_mode_switch_set_mode(&dwc->ums, mode);
    if (status != ZX_OK) {
      goto fail;
    }
  }

  if (mode == USB_MODE_PERIPHERAL) {
    status = pdev_get_interrupt(&dwc->pdev, IRQ_USB3, 0, dwc->irq_handle.reset_and_get_address());
    if (status != ZX_OK) {
      zxlogf(ERROR, "dwc3_set_mode: pdev_get_interrupt failed\n");
      goto fail;
    }

    dwc3_start_peripheral_mode(dwc);
  } else if (mode == USB_MODE_HOST) {
    dwc3_start_host_mode(dwc);
  }

  dwc->usb_mode = mode;
  return ZX_OK;

fail:
  if (dwc->ums.ops != nullptr) {
    usb_mode_switch_set_mode(&dwc->ums, USB_MODE_NONE);
  }
  dwc->usb_mode = USB_MODE_NONE;

  return status;
}

usb_mode_switch_protocol_ops_t dwc_ums_ops = {
    .set_mode = dwc3_set_mode,
};

static void dwc3_unbind(void* ctx) {
  auto* dwc = static_cast<dwc3_t*>(ctx);
  dwc->irq_handle.destroy();
  thrd_join(dwc->irq_thread, nullptr);
  device_remove(dwc->zxdev);
}

static zx_status_t dwc3_get_protocol(void* ctx, uint32_t proto_id, void* out) {
  switch (proto_id) {
    case ZX_PROTOCOL_USB_DCI: {
      auto proto = static_cast<usb_dci_protocol_t*>(out);
      proto->ops = &dwc_dci_ops;
      proto->ctx = ctx;
      return ZX_OK;
    }
    case ZX_PROTOCOL_USB_MODE_SWITCH: {
      auto proto = static_cast<usb_mode_switch_protocol_t*>(out);
      proto->ops = &dwc_ums_ops;
      proto->ctx = ctx;
      return ZX_OK;
    }
    default:
      return ZX_ERR_NOT_SUPPORTED;
  }
}

static void dwc3_release(void* ctx) {
  auto* dwc = static_cast<dwc3_t*>(ctx);

  for (unsigned i = 0; i < countof(dwc->eps); i++) {
    dwc3_ep_fifo_release(dwc, i);
  }
  io_buffer_release(&dwc->event_buffer);
  io_buffer_release(&dwc->ep0_buffer);
  delete dwc;
}

static zx_protocol_device_t dwc3_device_ops = []() {
  zx_protocol_device_t device;
  device.version = DEVICE_OPS_VERSION;
  device.get_protocol = dwc3_get_protocol;
  device.release = dwc3_release;
  return device;
}();

zx_status_t dwc3_bind(void* ctx, zx_device_t* parent) {
  zxlogf(INFO, "dwc3_bind\n");

  auto* dwc = new dwc3_t;
  if (!dwc) {
    return ZX_ERR_NO_MEMORY;
  }
  list_initialize(&dwc->pending_completions);

  composite_protocol_t composite;
  zx_status_t status = device_get_protocol(parent, ZX_PROTOCOL_COMPOSITE, &composite);
  if (status != ZX_OK) {
    zxlogf(ERROR, "%s: Could not get ZX_PROTOCOL_COMPOSITE\n", __func__);
    goto fail;
  }
  zx_device_t* components[COMPONENT_COUNT];
  size_t actual;
  composite_get_components(&composite, components, COMPONENT_COUNT, &actual);
  if (actual != COMPONENT_COUNT) {
    zxlogf(ERROR, "%s: Could not get components\n", __func__);
    goto fail;
  }

  status = device_get_protocol(components[COMPONENT_PDEV], ZX_PROTOCOL_PDEV, &dwc->pdev);
  if (status != ZX_OK) {
    zxlogf(ERROR, "%s: Could not get ZX_PROTOCOL_PDEV\n", __func__);
    goto fail;
  }

  status = device_get_protocol(components[COMPONENT_UMS], ZX_PROTOCOL_USB_MODE_SWITCH, &dwc->ums);
  if (status != ZX_OK) {
    zxlogf(ERROR, "%s: Could not get ZX_PROTOCOL_USB_MODE_SWITCH\n", __func__);
    goto fail;
  }

  status = pdev_get_bti(&dwc->pdev, 0, dwc->bti_handle.reset_and_get_address());
  if (status != ZX_OK) {
    goto fail;
  }

  for (uint8_t i = 0; i < countof(dwc->eps); i++) {
    dwc3_endpoint_t* ep = &dwc->eps[i];
    ep->ep_num = i;
    list_initialize(&ep->queued_reqs);
  }
  dwc->parent = parent;
  dwc->pdev_dev = components[COMPONENT_PDEV];
  dwc->usb_mode = USB_MODE_NONE;

  mmio_buffer_t mmio;
  status = pdev_map_mmio_buffer(&dwc->pdev, MMIO_USB3OTG, ZX_CACHE_POLICY_UNCACHED_DEVICE, &mmio);
  if (status != ZX_OK) {
    zxlogf(ERROR, "dwc3_bind: pdev_map_mmio_buffer failed\n");
    goto fail;
  }
  dwc->mmio = ddk::MmioBuffer(mmio);

  status = io_buffer_init(&dwc->event_buffer, dwc->bti_handle.get(), EVENT_BUFFER_SIZE,
                          IO_BUFFER_RO | IO_BUFFER_CONTIG);
  if (status != ZX_OK) {
    zxlogf(ERROR, "dwc3_bind: io_buffer_init failed\n");
    goto fail;
  }
  io_buffer_cache_flush(&dwc->event_buffer, 0, EVENT_BUFFER_SIZE);

  status = io_buffer_init(&dwc->ep0_buffer, dwc->bti_handle.get(), UINT16_MAX,
                          IO_BUFFER_RW | IO_BUFFER_CONTIG);
  if (status != ZX_OK) {
    zxlogf(ERROR, "dwc3_bind: io_buffer_init failed\n");
    goto fail;
  }

  status = dwc3_ep0_init(dwc);
  if (status != ZX_OK) {
    zxlogf(ERROR, "dwc3_bind: dwc3_ep_init failed\n");
    goto fail;
  }

  {
    device_add_args_t args = {};
    args.version = DEVICE_ADD_ARGS_VERSION;
    args.name = "dwc3";
    args.ctx = dwc;
    args.ops = &dwc3_device_ops;
    args.proto_id = ZX_PROTOCOL_USB_DCI;
    args.proto_ops = &dwc_dci_ops,

    status = device_add(parent, &args, &dwc->zxdev);
    if (status != ZX_OK) {
      goto fail;
    }
  }

  return ZX_OK;

fail:
  zxlogf(ERROR, "dwc3_bind failed %d\n", status);
  dwc3_release(dwc);
  return status;
}

static constexpr zx_driver_ops_t dwc3_driver_ops = []() {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = dwc3_bind;
  return ops;
}();

// clang-format off
ZIRCON_DRIVER_BEGIN(dwc3, dwc3_driver_ops, "zircon", "0.1", 4)
    BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_COMPOSITE),
    BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_GENERIC),
    BI_ABORT_IF(NE, BIND_PLATFORM_DEV_PID, PDEV_PID_GENERIC),
    BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_USB_DWC3),
ZIRCON_DRIVER_END(dwc3)
    // clang-format on
